/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <gdbus.h>

#include <connman/assert.h>
#include <connman/inet.h>

#include "connman.h"

#define	_DBG_DEVICE(fmt, arg...)	DBG(DBG_DEVICE, fmt, ## arg)

static DBusConnection *connection = NULL;

struct connman_device {
	struct connman_element element;		/* base class */
	enum connman_device_type type;
	enum connman_device_mode mode;
	connman_bool_t registered;
	connman_bool_t offlinemode;
	connman_bool_t blocked;
	connman_bool_t powered;
	connman_bool_t powered_pending;
	connman_bool_t powered_persistent;
	connman_bool_t carrier;
	connman_bool_t scanning;
	connman_bool_t connected;
	connman_bool_t reconnect;
	struct connman_rtnl rtnl;		/* RTNL watch for dev rename */
	connman_uint16_t scan_interval;		/* long scan interval (secs) */
	connman_uint16_t scan_short;		/* short scan interval (secs) */
	int signal_threshold;			/* short/long threshold (dBm) */
	char *scan_method;			/* background scan method */
	char *name;
	char *node;
	char *address;				/* local address */
	char *bssid;				/* connected address */
	char *interface;
	char *control;
	char *ident;
	int phyindex;
	uint32_t configmask;			/* bitmask of ip configs */
	guint scan_timeout;
	struct {
		enum connman_device_cellular_family family;
		char *carrier;
		char *esn;
		char *meid;
		char *imei;
		char *imsi;
		char *mdn;
		char *min;
		char *model_id;
		char *manufacturer;
		char *firmware_revision;
		char *firmware_image;
		char *hardware_revision;
		char *unlock_required;
		char *selected_network;
		guint prl_version;
		guint unlock_retries;
		connman_bool_t lock_enabled;
		connman_bool_t roaming_allowed;
		connman_bool_t scanning_supported;
		GPtrArray *found_networks;
		GPtrArray *apn_list;
		struct connman_network_operator home_provider;
		connman_bool_t provider_requires_roaming;
	} cellular;

	struct connman_device_driver *driver;
	void *driver_data;

	struct connman_network *network;	/* current connected network */
	GHashTable *networks;			/* associated networks */

	DBusMessage *pending;
	guint timeout;

        /* Address and path of an external DBus object backing this device. */
	char *external_dbus_connection;
	char *external_dbus_service;
	char *external_dbus_object;

	/* count of times the reverse path filter has been disabled */
	int rp_filter_count;
};

static const char *kStringDictArraySig =
	DBUS_TYPE_ARRAY_AS_STRING
	  DBUS_TYPE_ARRAY_AS_STRING
	    DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
	      DBUS_TYPE_STRING_AS_STRING
	      DBUS_TYPE_STRING_AS_STRING
	    DBUS_DICT_ENTRY_END_CHAR_AS_STRING;

#define DEFAULT_SCAN_METHOD NULL /* Leave it up to the device plugin */
#define DEFAULT_SCAN_INTERVAL 180
#define DEFAULT_SCAN_SHORT 30
#define DEFAULT_SIGNAL_THRESHOLD -50

#define SCAN_INTERVAL_UNSET ((connman_uint16_t)-1)
#define SIGNAL_THRESHOLD_UNSET 9999

static void device_newlink(void *user_data, int index, unsigned short type,
    const char *ifname, unsigned flags, int change);

/*
 * Scan timer handling.
 */

/*
 * Timer callback to do a bgscan request.  We assume bgscan operations
 * while connected are handled elsewhere (e.g. wpa_supplicant for wifi,
 * bluez for bluetooth).  If a device needs explicit scan requests made
 * while connected the device can schedule it's own timer and use the
 * connman_device_scan request below.
 */
static gboolean device_scan(gpointer user_data)
{
	struct connman_device *device = user_data;

	_DBG_DEVICE("device %p", device);

	if (device->driver == NULL) {
		device->scan_timeout = 0;
		return FALSE;
	}
	if (device->connected == FALSE) {
		/* TODO(sleffler) can driver->scan ever be NULL? */
		if (device->driver->scan)
			device->driver->scan(device);
	} else {
		/*
		 * NB: this can happen during suspend/resume so just drop
		 * the request; the timer should be cleared shortly.
		 */
		connman_warn("%s: scanning while connected", device->name);
	}
	return TRUE;
}

/*
 * Stop and disable bgscan work.
 */
static void bgscan_disable(struct connman_device *device)
{
	if (device->scan_timeout != 0) {
		g_source_remove(device->scan_timeout);
		device->scan_timeout = 0;
	}
}

/*
 * Enable bgscan work using the current settings.
 */
static void bgscan_enable(struct connman_device *device)
{
	int scan_interval = connman_device_bgscan_get_long(device);
	bgscan_disable(device);

	if (scan_interval > 0) {
		guint interval = scan_interval;
		device->scan_timeout = g_timeout_add_seconds(interval,
					device_scan, device);
	}
}

static const char *type2description(enum connman_device_type type)
{
	switch (type) {
	case CONNMAN_DEVICE_TYPE_UNKNOWN:
	case CONNMAN_DEVICE_TYPE_VENDOR:
		break;
	case CONNMAN_DEVICE_TYPE_ETHERNET:
		return "Ethernet";
	case CONNMAN_DEVICE_TYPE_WIFI:
		return "Wireless";
	case CONNMAN_DEVICE_TYPE_WIMAX:
		return "WiMAX";
	case CONNMAN_DEVICE_TYPE_BLUETOOTH:
		return "Bluetooth";
	case CONNMAN_DEVICE_TYPE_GPS:
		return "GPS";
	case CONNMAN_DEVICE_TYPE_CELLULAR:
		return "Cellular";
	}

	return NULL;
}

static const char *type2string(enum connman_device_type type)
{
	switch (type) {
	case CONNMAN_DEVICE_TYPE_UNKNOWN:
	case CONNMAN_DEVICE_TYPE_VENDOR:
		break;
	case CONNMAN_DEVICE_TYPE_ETHERNET:
		return "ethernet";
	case CONNMAN_DEVICE_TYPE_WIFI:
		return "wifi";
	case CONNMAN_DEVICE_TYPE_WIMAX:
		return "wimax";
	case CONNMAN_DEVICE_TYPE_BLUETOOTH:
		return "bluetooth";
	case CONNMAN_DEVICE_TYPE_GPS:
		return "gps";
	case CONNMAN_DEVICE_TYPE_CELLULAR:
		return "cellular";
	}
	return NULL;
}

static const char *family2string(enum connman_device_cellular_family family)
{
	switch (family) {
	case CONNMAN_DEVICE_CELLULAR_FAMILY_CDMA:
		return "CDMA";
	case CONNMAN_DEVICE_CELLULAR_FAMILY_GSM:
		return "GSM";
	}
	return NULL;
}

enum connman_service_type __connman_device_get_service_type(
    struct connman_device *device)
{
	enum connman_device_type type = connman_device_get_type(device);

	switch (type) {
	case CONNMAN_DEVICE_TYPE_UNKNOWN:
	case CONNMAN_DEVICE_TYPE_VENDOR:
	case CONNMAN_DEVICE_TYPE_GPS:
		break;
	case CONNMAN_DEVICE_TYPE_ETHERNET:
		return CONNMAN_SERVICE_TYPE_ETHERNET;
	case CONNMAN_DEVICE_TYPE_WIFI:
		return CONNMAN_SERVICE_TYPE_WIFI;
	case CONNMAN_DEVICE_TYPE_WIMAX:
		return CONNMAN_SERVICE_TYPE_WIMAX;
	case CONNMAN_DEVICE_TYPE_BLUETOOTH:
		return CONNMAN_SERVICE_TYPE_BLUETOOTH;
	case CONNMAN_DEVICE_TYPE_CELLULAR:
		return CONNMAN_SERVICE_TYPE_CELLULAR;
	}

	return CONNMAN_SERVICE_TYPE_UNKNOWN;
}

struct connman_ipconfig *connman_device_add_ipconfig(
    struct connman_device *device, enum connman_ipconfig_type type)
{
	struct connman_ipconfig *ipconfig;
	gchar name[32];
	int ix;

	/* XXX not unique if config's deleted */
	ix = ffs(~(device->configmask));
	if (ix == 0) {
		/* XXX should not happen */
		connman_error("%s: too many ipconfigs for %s", __func__,
		    device->name);
		return NULL;
	}
	g_snprintf(name, sizeof(name), "%s_%d", device->ident, ix-1);
	ipconfig = connman_ipconfig_create(type, name, device,
	    device->element.index, TRUE);
	if (ipconfig == NULL) {
		connman_error("%s: failed to create %s ipconfig for %s",
		    __func__, type2string(type), device->name);
		return NULL;
	}
	/* ipconfig's are children of the device */
	/* TODO(sleffler) check return value */
	connman_ipconfig_register(ipconfig, &device->element);
	device->configmask |= 1<<(ix-1);

	__connman_profile_save_device(device);

	return ipconfig;
}

/**
 * set_connected
 * @device: connman device that is now (dis)connected
 * @connected: is device connected or disconnected
 *
 * Returns: %0 on success
 */
static int set_connected(struct connman_device *device,
    connman_bool_t connected)
{
	struct connman_service *service =
	    __connman_service_lookup_from_device(device);
	enum connman_service_type type =
	    __connman_device_get_service_type(device);

	if (connected == TRUE) {
		connman_bool_t was_previously_connected;
		device->connected = TRUE;
		bgscan_disable(device);

		/* kick off L3 configuration */
		if (device->configmask == 0) {
			/*
			 * Auto-add DHCP if no ipconfig records are set up
			 * and the device is backed by a network interface.
			 *
			 * TODO(sleffler) this is policy, perhaps add a
			 * config knob to disable
			 */
			connman_device_add_ipconfig(device,
						    CONNMAN_IPCONFIG_TYPE_DHCP);
		}
		was_previously_connected =
			__connman_service_get_connected(service);
		connman_ipconfig_request(device);
		/*
		 * We manually ena/clear ipv6 routes and IP addresses because
		 * we rely on the kernel to install them and they are only
		 * cleared if the interface is marked down--which does not
		 * happen in flimflam.
		 */
		connman_inet_enable_ipv6(connman_device_get_index(device));

		/*
		 * Some ipconfig types immediately mark the service as
		 * connected.  We can detect this by checking whether
		 * the service state has changed from not-connected
		 * to connected since before we called
		 * connman_ipconfig_request above.  If the the service
		 * was connected as a result of conman_ipconfig_request,
		 * do NOT mark the service back in the "configuring" state.
		 */
		if (!(was_previously_connected == FALSE &&
		      __connman_service_get_connected(service) == TRUE)) {
			__connman_service_indicate_state(service,
					 CONNMAN_SERVICE_STATE_CONFIGURATION);
		}
		__connman_notifier_connect(type);
	} else {
		/* tear down L3 configuration */
		connman_inet_disable_ipv6(connman_device_get_index(device));
		connman_ipconfig_release(device);

		device->connected = FALSE;
		__connman_notifier_disconnect(type);
		__connman_service_indicate_state(service,
		    CONNMAN_SERVICE_STATE_IDLE);
		bgscan_enable(device);
	}
	return 0;
}

static int set_carrier(struct connman_device *device, connman_bool_t carrier)
{
	if (carrier == TRUE)
		__connman_profile_add_device(device);
	else
		__connman_profile_remove_device(device);

	return set_connected(device, carrier);
}

static connman_bool_t powered_changed(struct connman_device *device)
{
	return connman_dbus_send_property_changed_variant(device->element.path,
	    CONNMAN_DEVICE_INTERFACE, "Powered",
	    DBUS_TYPE_BOOLEAN, &device->powered);
}

static int set_powered(struct connman_device *device, connman_bool_t powered)
{
	struct connman_device_driver *driver = device->driver;
	enum connman_service_type type;
	int err;

	_DBG_DEVICE("device %p powered %d (currently %d/%d)",
		    device, powered,
		    device->powered, device->powered_pending);

	if (device->powered_pending == powered)
		return -EALREADY;

	if (driver == NULL)
		return -EINVAL;

	type = __connman_device_get_service_type(device);

	if (powered == TRUE) {
		if (driver->enable != NULL) {
			device->powered_pending = powered;
			err = driver->enable(device);
			if (err == 0)
				__connman_notifier_enable(type);
		} else
			err = -EINVAL;
	} else {
		device->powered_pending = powered;
		device->reconnect = FALSE;
		bgscan_disable(device);
		g_hash_table_remove_all(device->networks);
		set_carrier(device, FALSE);

		if (driver->disable != NULL) {
			err = driver->disable(device);
			if (err == 0)
				__connman_notifier_disable(type);
		} else
			err = -EINVAL;
	}

	if (err == 0) {
		device->powered = powered;
		if (device->registered == TRUE)
			powered_changed(device);
	} else if (err == -EINPROGRESS) {
		/* Not really an error, do not roll back powered_pending */
	} else {
		/* Roll back powered_pending.  No-op in -EINVAL case
		 * because powered_pending is already !powered */
		device->powered_pending = !powered;
	}
	return err;
}

/*
 * Append object paths for each network associated with a device.
 */
static void append_network_path(gpointer key, gpointer value,
    gpointer user_data)
{
	struct connman_element *element = value;
	DBusMessageIter *iter = user_data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
	    &element->path);
}

static void append_networks(DBusMessageIter *dict, void *arg)
{
	struct connman_device *device = arg;

	g_hash_table_foreach(device->networks, append_network_path, dict);
}

/*
 * Append object paths for each ipconfig associated with a device.
 */
static void append_ipconfig_path(struct connman_ipconfig *ipconfig, void *arg)
{
	const char *str = connman_ipconfig_get_path(ipconfig);
	DBusMessageIter *iter = arg;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &str);
}

static void append_ipconfig(DBusMessageIter *dict, void *arg)
{
	struct connman_device *device = arg;

	__connman_ipconfig_foreach(device, append_ipconfig_path, dict);
}

static void append_unlock_status(DBusMessageIter *dict, void *arg)
{
	struct connman_device *device = arg;
	if (device->cellular.unlock_required) {
		connman_dbus_dict_append_basic(dict, "LockType",
					       DBUS_TYPE_STRING,
					       &device->cellular.unlock_required);
		connman_dbus_dict_append_basic(dict, "RetriesLeft",
					       DBUS_TYPE_UINT32,
					       &device->cellular.unlock_retries);
		connman_dbus_dict_append_basic(dict, "LockEnabled",
					       DBUS_TYPE_BOOLEAN,
					       &device->cellular.lock_enabled);
	}
}

static void append_string_dict_item(gpointer key, gpointer value, gpointer data)
{
	DBusMessageIter *iter = data;
	connman_dbus_dict_append_string(iter, key, value);
}

static void append_string_dict_array(DBusMessageIter *iter, gpointer data)
{
	DBusMessageIter array, dict;
	int i;
	GPtrArray *arrayptr = (GPtrArray *)data;
	const char *array_sig = kStringDictArraySig
			+ strlen(DBUS_TYPE_ARRAY_AS_STRING);
	const char *dict_sig = array_sig
			+ strlen(DBUS_TYPE_ARRAY_AS_STRING);

	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
					 array_sig, &array);
	for (i = 0; i < arrayptr->len; i++) {
		GHashTable *props = g_ptr_array_index(arrayptr, i);
		dbus_message_iter_open_container(&array, DBUS_TYPE_ARRAY,
						 dict_sig, &dict);
		g_hash_table_foreach(props, append_string_dict_item, &dict);
		dbus_message_iter_close_container(&array, &dict);
	}
	dbus_message_iter_close_container(iter, &array);
}

static void append_operator(DBusMessageIter *iter, void *arg)
{
	struct connman_network_operator *op =
		(struct connman_network_operator *)arg;

	if (op->name != NULL)
		connman_dbus_dict_append_string(iter, "name", op->name);
	if (op->country != NULL)
		connman_dbus_dict_append_string(iter, "country", op->country);
	if (op->code != NULL)
		connman_dbus_dict_append_string(iter, "code", op->code);
}

/*
 * Count of the times the reverse path filter has been disabled on all
 * devices.  This is used to control the "all" reverse path filter.
 */
static int rp_filter_all_count;

static void rp_filter_set(const char *interface, connman_bool_t enabled)
{
	/* gcc 4.6.0 still warns on (void) write(...), hence unused */
        int fd, cnt __attribute__ ((unused));
        char filename[PATH_MAX];
        const char *str_value = (enabled == TRUE) ? "1" : "0";

        snprintf(filename, sizeof(filename),
                 "/proc/sys/net/ipv4/conf/%s/rp_filter", interface);
        fd = open(filename, O_WRONLY);
        if (fd == -1)
                return;
        cnt = write(fd, str_value, strlen(str_value));
        close(fd);
}

/**
 * connman_device_rp_filter_disable:
 * @device: device structure
 *
 * Disable the reverse path filter for this device.  Calls to disable
 * are counted, and only the first call changes the setting.
 *
 * If this is the first device to disable the reverse path filter,
 * disable the reverse path filter globally also.
 */
void connman_device_rp_filter_disable(struct connman_device *device)
{
	if (device->rp_filter_count == 0)
		rp_filter_set(device->interface, FALSE);
	device->rp_filter_count++;

	if (rp_filter_all_count == 0)
                rp_filter_set("all", FALSE);
	rp_filter_all_count++;
}

/**
 * connman_device_rp_filter_disable:
 * @device: device structure
 *
 * Enable the reverse path filter for this device.  Calls to disable
 * are counted, and only when the count gets to zero is the filter
 * enabled.
 *
 * If all devices have enabled the reverse path filter, then globally
 * enable it.
 */
void connman_device_rp_filter_enable(struct connman_device *device)
{
	device->rp_filter_count--;
	if (device->rp_filter_count == 0)
		rp_filter_set(device->interface, TRUE);

	rp_filter_all_count--;
	if (rp_filter_all_count == 0)
                rp_filter_set("all", TRUE);
}

static DBusMessage *get_properties(DBusConnection *conn, DBusMessage *msg,
    void *data)
{
	struct connman_device *device = data;
	DBusMessage *reply;
	DBusMessageIter array, dict;
	const char *str;
	int val;

	_DBG_DEVICE("conn %p", conn);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_PUBLIC) < 0)
		return __connman_error_permission_denied(msg);

	reply = dbus_message_new_method_return(msg);
	if (reply == NULL)
		return NULL;

	dbus_message_iter_init_append(reply, &array);

	dbus_message_iter_open_container(&array, DBUS_TYPE_ARRAY,
			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
			DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
			DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict);

	if (device->name != NULL)
		connman_dbus_dict_append_variant(&dict, "Name",
					DBUS_TYPE_STRING, &device->name);

	str = type2string(device->type);
	if (str != NULL)
		connman_dbus_dict_append_variant(&dict, "Type",
						DBUS_TYPE_STRING, &str);

	if (device->address != NULL)
		connman_dbus_dict_append_variant(&dict, "Address",
					DBUS_TYPE_STRING, &device->address);

	if (device->interface != NULL)
		connman_dbus_dict_append_variant(&dict, "Interface",
					DBUS_TYPE_STRING, &device->interface);

	connman_dbus_dict_append_variant(&dict, "Powered",
					DBUS_TYPE_BOOLEAN, &device->powered);

	if (device->driver && device->driver->scan)
		connman_dbus_dict_append_variant(&dict, "Scanning",
					DBUS_TYPE_BOOLEAN, &device->scanning);

	connman_dbus_dict_append_variant(&dict, "Reconnect",
					DBUS_TYPE_BOOLEAN, &device->reconnect);

	connman_dbus_dict_append_variant_array(&dict, "IPConfigs",
	    DBUS_TYPE_OBJECT_PATH, append_ipconfig, device);

	if (device->type == CONNMAN_DEVICE_TYPE_CELLULAR) {
		str = family2string(device->cellular.family);
		if (str != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.Family",
					DBUS_TYPE_STRING, &str);
		if (device->cellular.carrier != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.Carrier",
					DBUS_TYPE_STRING,
					&device->cellular.carrier);
		if (device->cellular.imsi != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.IMSI",
					DBUS_TYPE_STRING,
					&device->cellular.imsi);
		if (device->cellular.imei != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.IMEI",
					DBUS_TYPE_STRING,
					&device->cellular.imei);
		if (device->cellular.meid != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.MEID",
					DBUS_TYPE_STRING,
					&device->cellular.meid);
		if (device->cellular.esn != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.ESN",
					DBUS_TYPE_STRING,
					&device->cellular.esn);
		if (device->cellular.mdn != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.MDN",
					DBUS_TYPE_STRING,
					&device->cellular.mdn);
		if (device->cellular.min != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.MIN",
					DBUS_TYPE_STRING,
					&device->cellular.min);
		if (device->cellular.model_id != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.ModelID",
					DBUS_TYPE_STRING,
					&device->cellular.model_id);
		if (device->cellular.manufacturer != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.Manufacturer",
					DBUS_TYPE_STRING,
					&device->cellular.manufacturer);
		if (device->cellular.firmware_revision != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.FirmwareRevision",
					DBUS_TYPE_STRING,
					&device->cellular.firmware_revision);
		if (device->cellular.firmware_image != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.FirmwareImageName",
					DBUS_TYPE_STRING,
					&device->cellular.firmware_image);
		if (device->cellular.hardware_revision != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.HardwareRevision",
					DBUS_TYPE_STRING,
					&device->cellular.hardware_revision);
		if (device->cellular.home_provider.name != NULL)
                        connman_dbus_dict_append_string_dict(
                                &dict, "Cellular.HomeProvider",
                                append_operator,
                                &device->cellular.home_provider);
		connman_dbus_dict_append_variant(
				&dict, "Cellular.PRLVersion",
				DBUS_TYPE_UINT16,
				&device->cellular.prl_version);
		connman_dbus_dict_append_dict(&dict, "Cellular.SIMLockStatus",
				      append_unlock_status, device);
		if (device->cellular.selected_network != NULL)
			connman_dbus_dict_append_variant(
					&dict, "Cellular.SelectedNetwork",
					DBUS_TYPE_STRING,
					&device->cellular.selected_network);
		if (device->cellular.found_networks != NULL)
			connman_dbus_dict_append_variant_container(&dict,
					"Cellular.FoundNetworks",
					kStringDictArraySig,
					append_string_dict_array,
					device->cellular.found_networks);
		if (device->cellular.apn_list != NULL)
			connman_dbus_dict_append_variant_container(&dict,
					"Cellular.APNList",
					kStringDictArraySig,
					append_string_dict_array,
					device->cellular.apn_list);
		connman_dbus_dict_append_variant(&dict, "Cellular.AllowRoaming",
					DBUS_TYPE_BOOLEAN,
					&device->cellular.roaming_allowed);
		connman_dbus_dict_append_variant(
				&dict,
				"Cellular.SupportNetworkScan",
				DBUS_TYPE_BOOLEAN,
				&device->cellular.scanning_supported);
		connman_dbus_dict_append_variant(&dict, "Cellular.ProviderRequiresRoaming",
						 DBUS_TYPE_BOOLEAN,
						 &device->cellular.provider_requires_roaming);
	}

	if (device->external_dbus_connection != NULL)
		connman_dbus_dict_append_variant(&dict, "DBus.Connection",
                        DBUS_TYPE_STRING, &device->external_dbus_connection);
	if (device->external_dbus_service != NULL)
		connman_dbus_dict_append_variant(&dict, "DBus.Service",
                        DBUS_TYPE_STRING, &device->external_dbus_service);
	if (device->external_dbus_object != NULL)
		connman_dbus_dict_append_basic(&dict, "DBus.Object",
			DBUS_TYPE_OBJECT_PATH, &device->external_dbus_object);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		break;
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		val = connman_device_bgscan_get_long(device);
		if (val > 0)
			connman_dbus_dict_append_variant(&dict, "ScanInterval",
				DBUS_TYPE_UINT16, &val);
		str = connman_device_bgscan_get_method(device);
		if (str != NULL)
			connman_dbus_dict_append_variant(&dict, "BgscanMethod",
				DBUS_TYPE_STRING, &str);
		val = connman_device_bgscan_get_short(device);
		if (val > 0)
			connman_dbus_dict_append_variant(&dict,
			    "BgscanShortInterval", DBUS_TYPE_UINT16, &val);
		val = connman_device_bgscan_get_signal_threshold(device);
		if (val != 0)
			connman_dbus_dict_append_variant(&dict,
			    "BgscanSignalThreshold", DBUS_TYPE_INT32, &val);

		connman_dbus_dict_append_variant_array(&dict, "Networks",
		    DBUS_TYPE_OBJECT_PATH, append_networks, device);
		break;
	}

	dbus_message_iter_close_container(&array, &dict);

	return reply;
}

static gboolean powered_timeout(gpointer user_data)
{
	struct connman_device *device = user_data;

	_DBG_DEVICE("device %p", device);

	device->timeout = 0;

	if (device->pending != NULL) {
		DBusMessage *reply;

		reply = __connman_error_operation_timeout(device->pending);
		if (reply != NULL)
			g_dbus_send_message(connection, reply);

		dbus_message_unref(device->pending);
		device->pending = NULL;
	}
	return FALSE;
}

static int check_bgscan_parameter(struct connman_device *device,
	DBusMessageIter *value, int type)
{
	if (device->mode == CONNMAN_DEVICE_MODE_UNKNOWN ||
	    device->mode == CONNMAN_DEVICE_MODE_TRANSPORT_IP)
		return 0;
	return (dbus_message_iter_get_arg_type(value) == type);
}

static DBusMessage *set_property(DBusConnection *conn, DBusMessage *msg,
    void *data)
{
	struct connman_device *device = data;
	DBusMessageIter iter, value;
	const char *name;

	_DBG_DEVICE("conn %p", conn);

	if (dbus_message_iter_init(msg, &iter) == FALSE)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &name);
	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (g_str_equal(name, "Powered") == TRUE) {
		connman_bool_t powered;
		int type, err;

		type = dbus_message_iter_get_arg_type(&value);
		if (type != DBUS_TYPE_BOOLEAN)
			return __connman_error_invalid_arguments(msg);

		dbus_message_iter_get_basic(&value, &powered);

		device->powered_persistent = powered;

		__connman_profile_save_device(device);

		if (device->powered == powered)
			return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);

		if (device->pending != NULL)
			return __connman_error_in_progress(msg);

		err = set_powered(device, powered);
		if (err < 0) {
			if (err != -EINPROGRESS)
				return __connman_error_failed(msg, -err);

			device->pending = dbus_message_ref(msg);

			device->timeout = g_timeout_add_seconds(15,
						powered_timeout, device);

			return NULL;
		}
	} else if (g_str_equal(name, "ScanInterval") == TRUE) {
		connman_uint16_t interval;

		if (!check_bgscan_parameter(device, &value, DBUS_TYPE_UINT16))
			return __connman_error_invalid_arguments(msg);
		dbus_message_iter_get_basic(&value, &interval);

		if (device->scan_interval != interval) {
			device->scan_interval = interval;
			__connman_profile_save_device(device);
			connman_element_update(&device->element);

			if (device->scan_timeout != 0)
				bgscan_enable(device);
		}
	} else if (g_str_equal(name, "BgscanMethod") == TRUE) {
		const char *method;

		if (!check_bgscan_parameter(device, &value, DBUS_TYPE_STRING))
			return __connman_error_invalid_arguments(msg);
		dbus_message_iter_get_basic(&value, &method);

		if (g_strcmp0(method, device->scan_method) != 0) {
			g_free(device->scan_method);
			device->scan_method = g_strdup(method);
			__connman_profile_save_device(device);
			connman_element_update(&device->element);
		}
	} else if (g_str_equal(name, "BgscanShortInterval") == TRUE) {
		connman_uint16_t interval;

		if (!check_bgscan_parameter(device, &value, DBUS_TYPE_UINT16))
			return __connman_error_invalid_arguments(msg);
		dbus_message_iter_get_basic(&value, &interval);

		if (device->scan_short != interval) {
			device->scan_short = interval;
			__connman_profile_save_device(device);
			connman_element_update(&device->element);
		}
	} else if (g_str_equal(name, "BgscanSignalThreshold") == TRUE) {
		int threshold;

		if (!check_bgscan_parameter(device, &value, DBUS_TYPE_INT32))
			return __connman_error_invalid_arguments(msg);
		dbus_message_iter_get_basic(&value, &threshold);

		if (device->signal_threshold != threshold) {
			device->signal_threshold = threshold;
			__connman_profile_save_device(device);
			connman_element_update(&device->element);
		}
        } else if (g_str_equal(name, "Cellular.AllowRoaming") &&
		   device->type == CONNMAN_DEVICE_TYPE_CELLULAR) {
		int type;
		dbus_bool_t allow;
		type = dbus_message_iter_get_arg_type(&value);
		if (type != DBUS_TYPE_BOOLEAN)
			return __connman_error_invalid_arguments(msg);
		dbus_message_iter_get_basic(&value, &allow);
		if (allow != device->cellular.roaming_allowed) {
			device->cellular.roaming_allowed = allow;
			__connman_profile_save_device(device);
			connman_dbus_send_property_changed_variant(device->element.path,
						   CONNMAN_DEVICE_INTERFACE,
						   "Cellular.AllowRoaming",
						   DBUS_TYPE_BOOLEAN, &allow);
		}
		if (!connman_device_roaming_allowed(device) &&
		    device->network != NULL) {
			struct connman_service *service =
				connman_service_lookup_from_network(device->network);
			if (service != NULL)
				__connman_service_disconnect_if_roaming(service);
		}
	} else
		return __connman_error_invalid_property(msg);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *clear_property(DBusConnection *conn, DBusMessage *msg,
    void *data)
{
	struct connman_device *device = data;
	DBusMessageIter iter;
	const char *name;

	_DBG_DEVICE("conn %p", conn);

	if (dbus_message_iter_init(msg, &iter) == FALSE)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(&iter, &name);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (g_str_equal(name, "ScanInterval") == TRUE) {
		device->scan_interval = SCAN_INTERVAL_UNSET;
		__connman_profile_save_device(device);
		connman_element_update(&device->element);
		if (device->scan_timeout != 0)
			bgscan_enable(device);
	} else if (g_str_equal(name, "BgscanMethod") == TRUE) {
		g_free(device->scan_method);
		device->scan_method = NULL;
		__connman_profile_save_device(device);
		connman_element_update(&device->element);
	} else if (g_str_equal(name, "BgscanShortInterval") == TRUE) {
		device->scan_short = SCAN_INTERVAL_UNSET;
		__connman_profile_save_device(device);
		connman_element_update(&device->element);
	} else if (g_str_equal(name, "BgscanSignalThreshold") == TRUE) {
		device->signal_threshold = SIGNAL_THRESHOLD_UNSET;
		__connman_profile_save_device(device);
		connman_element_update(&device->element);
	} else
		return __connman_error_invalid_property(msg);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *propose_scan(DBusConnection *conn, DBusMessage *msg,
    void *data)
{
	struct connman_device *device = data;
	int err;

	_DBG_DEVICE("conn %p", conn);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		return __connman_error_not_supported(msg);
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		break;
	}

	err = connman_device_scan(device);
	if (err < 0)
		return __connman_error_failed(msg, -err);

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *add_ipconfig(DBusConnection *conn, DBusMessage *msg,
    void *data)
{
	struct connman_device *device = data;
	struct connman_ipconfig *ipconfig;
	enum connman_ipconfig_type type;
	const char *str;

	_DBG_DEVICE("conn %p", conn);

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &str,
	    DBUS_TYPE_INVALID) == FALSE)
		return NULL;

	if (__connman_ipconfig_parse_type(str, &type) == FALSE)
		return __connman_error_invalid_arguments(msg);

	ipconfig = connman_device_add_ipconfig(device, type);
	if (ipconfig == NULL)
		return __connman_error_invalid_arguments(msg);	/* XXX */

	str = connman_ipconfig_get_path(ipconfig);
	return g_dbus_create_reply(msg, DBUS_TYPE_OBJECT_PATH, &str,
	    DBUS_TYPE_INVALID);
}

static const char *error_to_dbus_error_name(enum connman_element_error error)
{
	const char *errname;

	if (error == CONNMAN_ELEMENT_ERROR_PIN_REQUIRED)
		errname = CONNMAN_ERROR_INTERFACE ".PinRequired";
	else if (error == CONNMAN_ELEMENT_ERROR_PIN_BLOCKED)
		errname = CONNMAN_ERROR_INTERFACE ".PinBlocked";
	else if (error == CONNMAN_ELEMENT_ERROR_INCORRECT_PIN)
		errname = CONNMAN_ERROR_INTERFACE ".IncorrectPin";
	else
		errname = CONNMAN_ERROR_INTERFACE ".InternalError";
	return errname;
}

static void pin_method_callback(enum connman_element_error error,
				struct connman_dbus_method_callback *cb,
				void *data)
{
	DBusError dberror;

	_DBG_DEVICE("%s error: %d", connman_dbus_callback_method_name(cb), error);
	dbus_error_init(&dberror);
	if (error != CONNMAN_ELEMENT_ERROR_NO_ERROR) {
		dbus_set_error_const(&dberror,
			       error_to_dbus_error_name(error),
			       "PIN error");
	}
	connman_dbus_callback_send_reply(cb, &dberror);
	dbus_error_free(&dberror);
	connman_dbus_free_callback(cb);
}

static DBusMessage *require_pin(DBusConnection *conn, DBusMessage *msg,
				void *data)
{
	struct connman_device *device = data;
	struct connman_dbus_method_callback *callback;
	const char *pin;
	connman_bool_t require;
	int err;

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (dbus_message_get_args(msg, NULL,
				  DBUS_TYPE_STRING, &pin,
				  DBUS_TYPE_BOOLEAN, &require,
				  DBUS_TYPE_INVALID) == FALSE)
		return __connman_error_invalid_arguments(msg);

	if (device->driver == NULL || device->driver->require_pin == NULL)
		return __connman_error_not_supported(msg);

	callback = connman_dbus_callback_new(pin_method_callback, msg, NULL);
	err = device->driver->require_pin(device, pin, require, callback);
	if (err < 0) {
		connman_dbus_free_callback(callback);
		return __connman_error_failed(msg, -err);
	}
	return NULL;
}

static DBusMessage *enter_pin(DBusConnection *conn, DBusMessage *msg,
			      void *data)
{
	struct connman_device *device = data;
	struct connman_dbus_method_callback *callback;
	const char *pin;
	int err;

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (dbus_message_get_args(msg, NULL,
				  DBUS_TYPE_STRING, &pin,
				  DBUS_TYPE_INVALID) == FALSE)
		return __connman_error_invalid_arguments(msg);

	if (device->driver == NULL || device->driver->enter_pin == NULL)
		return __connman_error_not_supported(msg);

	callback = connman_dbus_callback_new(pin_method_callback, msg, NULL);
	err = device->driver->enter_pin(device, pin, callback);
	if (err < 0) {
		connman_dbus_free_callback(callback);
		return __connman_error_failed(msg, -err);
	}
	return NULL;
}

static DBusMessage *unblock_pin(DBusConnection *conn, DBusMessage *msg,
				void *data)
{
	struct connman_device *device = data;
	struct connman_dbus_method_callback *callback;
	const char *unblock_code;
	const char *pin;
	int err;

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (dbus_message_get_args(msg, NULL,
				  DBUS_TYPE_STRING, &unblock_code,
				  DBUS_TYPE_STRING, &pin,
				  DBUS_TYPE_INVALID) == FALSE)
		return __connman_error_invalid_arguments(msg);

	if (device->driver == NULL || device->driver->unblock_pin == NULL)
		return __connman_error_not_supported(msg);

	callback = connman_dbus_callback_new(pin_method_callback, msg, NULL);
	err = device->driver->unblock_pin(device, unblock_code, pin, callback);
	if (err < 0) {
		connman_dbus_free_callback(callback);
		return __connman_error_failed(msg, -err);
	}
	return NULL;
}

static DBusMessage *change_pin(DBusConnection *conn, DBusMessage *msg,
			       void *data)
{
	struct connman_device *device = data;
	struct connman_dbus_method_callback *callback;
	const char *old_pin, *new_pin;
	int err;

	if (__connman_security_check_privilege(msg,
					CONNMAN_SECURITY_PRIVILEGE_MODIFY) < 0)
		return __connman_error_permission_denied(msg);

	if (dbus_message_get_args(msg, NULL,
				  DBUS_TYPE_STRING, &old_pin,
				  DBUS_TYPE_STRING, &new_pin,
				  DBUS_TYPE_INVALID) == FALSE)
		return __connman_error_invalid_arguments(msg);

	if (device->driver == NULL || device->driver->change_pin == NULL)
		return __connman_error_not_supported(msg);

	callback = connman_dbus_callback_new(pin_method_callback, msg, NULL);
	err = device->driver->change_pin(device, old_pin, new_pin, callback);
	if (err < 0) {
		connman_dbus_free_callback(callback);
		return __connman_error_failed(msg, -err);
	}
	return NULL;
}

static void registration_callback(enum connman_element_error error,
				  struct connman_dbus_method_callback *cb,
				  void *data)
{
	struct connman_device *device = data;
	DBusError dberror;

	_DBG_DEVICE("%s error: %d", connman_dbus_callback_method_name(cb), error);
	dbus_error_init(&dberror);
	if (error != CONNMAN_ELEMENT_ERROR_NO_ERROR) {
		dbus_set_error_const(&dberror,
				     CONNMAN_ERROR_INTERFACE ".RegistrationFailed",
				     "Registration Failed");
		g_free(device->cellular.selected_network);
		device->cellular.selected_network = NULL;
	} else if (device->cellular.selected_network != NULL) {
		__connman_profile_save_device(device);
	}
	connman_device_unref(device);
	connman_dbus_callback_send_reply(cb, &dberror);
	dbus_error_free(&dberror);
	connman_dbus_free_callback(cb);
}

static DBusMessage *register_on_network(DBusConnection *conn,
					DBusMessage *msg,
					void *data)
{
	struct connman_device *device = data;
	struct connman_dbus_method_callback *callback;
	const char *network_id;
	int err;

	if (dbus_message_get_args(msg, NULL,
				  DBUS_TYPE_STRING, &network_id,
				  DBUS_TYPE_INVALID) == FALSE)
		return __connman_error_invalid_arguments(msg);

	if (device->driver == NULL || device->driver->register_on_network == NULL)
		return __connman_error_not_supported(msg);

	callback = connman_dbus_callback_new(registration_callback, msg,
					     connman_device_ref(device));
	err = device->driver->register_on_network(device, network_id, callback);
	if (err < 0) {
		connman_dbus_free_callback(callback);
		return __connman_error_failed(msg, -err);
	}
	/* Clear any previously saved selected network */
	connman_device_clear_selected_network(device);
	/* Selected network will be persisted if registration succeeds */
	if (strlen(network_id) != 0)
		device->cellular.selected_network = g_strdup(network_id);
	return NULL;
}

static GDBusMethodTable device_methods[] = {
	{ "GetProperties", "",      "a{sv}", get_properties },
	{ "SetProperty",   "sv",    "",      set_property,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ "ClearProperty", "s",     "",      clear_property },
	{ "ProposeScan",   "",      "",      propose_scan   },
	{ "AddIPConfig",   "s",    "o",      add_ipconfig   },
	{ "Register",	   "s",	    "",	     register_on_network,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ "RequirePin",    "sb",    "",      require_pin,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ "EnterPin",      "s",     "",      enter_pin,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ "UnblockPin",    "ss",    "",      unblock_pin,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ "ChangePin",     "ss",    "",      change_pin,
						G_DBUS_METHOD_FLAG_ASYNC },
	{ },
};

static GDBusSignalTable device_signals[] = {
	{ "PropertyChanged", "sv" },
	{ },
};

/* NB: needed to match type signature */
static void register_ipconfig(struct connman_ipconfig *ipconfig, void *arg)
{
	connman_ipconfig_register(ipconfig, arg);
}

static void append_devices(DBusMessageIter *iter, void *arg)
{
	__connman_element_list(NULL, CONNMAN_ELEMENT_TYPE_DEVICE, iter);
}
static void emit_devices_signal(void)
{
	connman_dbus_send_property_changed_array(CONNMAN_MANAGER_PATH,
	    CONNMAN_MANAGER_INTERFACE, "Device",
	    DBUS_TYPE_OBJECT_PATH, append_devices, NULL);
}

static int register_interface(struct connman_element *element)
{
	struct connman_device *device = element->device;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (g_dbus_register_interface(connection, element->path,
					CONNMAN_DEVICE_INTERFACE,
					device_methods, device_signals,
					NULL, device, NULL) == FALSE) {
		connman_error("Failed to register %s device", element->path);
		return -EIO;
	}
	device->registered = TRUE;
	emit_devices_signal();
	/* register ipconfig records loaded from the profile */
	__connman_ipconfig_foreach(device, register_ipconfig, element);
	return 0;
}

static void unregister_interface(struct connman_element *element)
{
	struct connman_device *device = element->device;

	_DBG_DEVICE("element %p name %s", element, element->name);

	device->registered = FALSE;
	emit_devices_signal();
	g_dbus_unregister_interface(connection, element->path,
						CONNMAN_DEVICE_INTERFACE);
}

static int setup_device(struct connman_device *device,
    struct connman_device_driver *driver)
{
	enum connman_service_type type;
	int err;

	_DBG_DEVICE("device %p driver %p", device, driver);

	device->driver = driver;

	err = register_interface(&device->element);
	if (err < 0) {
		if (device->driver->remove)
			device->driver->remove(device);
		device->driver = NULL;
		return err;
	}

	type = __connman_device_get_service_type(device);
	__connman_notifier_register(type);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		break;
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		if (device->carrier == TRUE)
			__connman_profile_add_device(device);
		break;
	}

	if (device->offlinemode == FALSE && device->powered_persistent == TRUE)
		__connman_device_enable(device);
	return 0;
}

static void probe_driver(struct connman_element *element, gpointer user_data)
{
	struct connman_device_driver *driver = user_data;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (element->device == NULL)
		return;
	if (element->device->driver != NULL)
		return;
	if (driver->type != element->device->type)
		return;
	if (driver->probe(element->device) == 0)
		setup_device(element->device, driver);
}

/*
 * Extract the index from the ipconfig name.
 */
static int parse_ipconfig_index(const char *tag)
{
	const char *cp = strrchr(tag, '_');
	return (cp == NULL ? -1 : atoi(cp+1));
}

/*
 * Reclaim state for ipconfig records bound to a device.
 * We clear the entry from the bitmask and delete the
 * in-memory copy of the ipconfig record.
 */
static void remove_ipconfig(struct connman_ipconfig *ipconfig, void *arg)
{
	struct connman_device *device = arg;
	struct connman_service *service =
	    __connman_service_lookup_from_device(device);
	connman_bool_t was_connected;
	int ix;

	_DBG_DEVICE("device %p ipconfig %p configmask %d", device, ipconfig,
		    device->configmask);
	ix = parse_ipconfig_index(connman_ipconfig_get_name(ipconfig));
	CONNMAN_ASSERT(0 <= ix && ix < 32);
	device->configmask &= ~(1<<ix);

	was_connected = __connman_service_get_connecting_or_connected(service);
	connman_ipconfig_delete(ipconfig);

	_DBG_DEVICE("was_connected: %d now_connected: %d configmask: %d",
		    was_connected,
		    __connman_service_get_connecting_or_connected(service),
		    device->configmask);
	if (device->connected == TRUE && was_connected == TRUE &&
	    __connman_service_get_connecting_or_connected(service) == FALSE &&
	    device->configmask != 0) {
		/*
		 * Removing this ipconfig caused the service to disconnect
		 * but the underlying device is still connected and other
		 * ipconfigs exist.  Restart the configuration process so
		 * the service is connected with the new ipconfig parameters.
		 */
		_DBG_DEVICE("reconnecting %p", device);
		set_connected(device, TRUE);
	}
}

/*
 * Public version of remove_ipconfig.
 */
void __connman_device_remove_ipconfig(struct connman_device *device,
    struct connman_ipconfig *ipconfig)
{
	remove_ipconfig(ipconfig, device);
}

static void remove_device(struct connman_device *device)
{
	enum connman_service_type type;

	_DBG_DEVICE("device %p", device);

	__connman_device_disable(device);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		break;
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		__connman_profile_remove_device(device);
		break;
	}

	__connman_ipconfig_foreach(device, remove_ipconfig, device);

	type = __connman_device_get_service_type(device);
	__connman_notifier_unregister(type);

	unregister_interface(&device->element);

	/* NB: callers all verify device->driver is not NULL */
	if (device->driver->remove != NULL)
		device->driver->remove(device);
	device->driver = NULL;
}

static void remove_driver(struct connman_element *element, gpointer user_data)
{
	struct connman_device_driver *driver = user_data;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (element->device != NULL && element->device->driver == driver)
		remove_device(element->device);
}

connman_bool_t __connman_device_has_driver(struct connman_device *device)
{
	return (device == NULL || device->driver == NULL) ?
	    FALSE : device->registered;
}

static GSList *driver_list = NULL;

static gint compare_priority(gconstpointer a, gconstpointer b)
{
	const struct connman_device_driver *driver1 = a;
	const struct connman_device_driver *driver2 = b;

	return driver2->priority - driver1->priority;
}

/**
 * connman_device_driver_register:
 * @driver: device driver definition
 *
 * Register a new device driver
 *
 * Returns: %0 on success
 */
int connman_device_driver_register(struct connman_device_driver *driver)
{
	_DBG_DEVICE("driver %p name %s", driver, driver->name);

	/* TODO(sleffler) check return value */
	driver_list = g_slist_insert_sorted(driver_list, driver,
							compare_priority);
	__connman_element_foreach(NULL, CONNMAN_ELEMENT_TYPE_DEVICE,
						probe_driver, driver);
	return 0;
}

/**
 * connman_device_driver_unregister:
 * @driver: device driver definition
 *
 * Remove a previously registered device driver
 */
void connman_device_driver_unregister(struct connman_device_driver *driver)
{
	_DBG_DEVICE("driver %p name %s", driver, driver->name);

	driver_list = g_slist_remove(driver_list, driver);
	__connman_element_foreach(NULL, CONNMAN_ELEMENT_TYPE_DEVICE,
						remove_driver, driver);
}

static void unregister_network(gpointer data)
{
	struct connman_network *network = data;

	_DBG_DEVICE("network %p", network);

	connman_element_unregister((struct connman_element *) network);

	connman_network_unref(network);
}

static void device_destruct(struct connman_element *element)
{
	struct connman_device *device = element->device;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (device->timeout > 0) {
		g_source_remove(device->timeout);
		device->timeout = 0;
	}
	if (device->pending != NULL) {
		dbus_message_unref(device->pending);
		device->pending = NULL;
	}

	/* NB: tear down rtnl monitoring */
	connman_device_set_index(device, -1);

	g_free(device->ident);
	g_free(device->node);
	g_free(device->name);
	g_free(device->address);
	g_free(device->control);
	g_free(device->interface);
	g_free(device->cellular.carrier);
	g_free(device->cellular.meid);
	g_free(device->cellular.imei);
	g_free(device->cellular.imsi);
	g_free(device->cellular.esn);
	g_free(device->cellular.mdn);
	g_free(device->cellular.min);
	g_free(device->cellular.manufacturer);
	g_free(device->cellular.model_id);
	g_free(device->cellular.hardware_revision);
	g_free(device->cellular.home_provider.code);
	g_free(device->cellular.home_provider.name);
	g_free(device->cellular.home_provider.country);
	g_free(device->cellular.firmware_revision);
	g_free(device->cellular.firmware_image);
	g_free(device->cellular.unlock_required);
	g_free(device->cellular.selected_network);
	g_free(device->external_dbus_connection);
	g_free(device->external_dbus_service);
	g_free(device->external_dbus_object);

	g_hash_table_destroy(device->networks);
	if (device->cellular.found_networks != NULL)
		g_ptr_array_free(device->cellular.found_networks, TRUE);
	if (device->cellular.apn_list != NULL)
		g_ptr_array_free(device->cellular.apn_list, TRUE);
	device->networks = NULL;
}

/**
 * connman_device_create:
 * @node: device node name (for example an address)
 * @type: device type
 *
 * Allocate a new device of given #type and name it #node.
 *
 * Returns: a newly-allocated #connman_device structure
 */
struct connman_device * connman_device_create(const char *node,
    enum connman_device_type type)
{
	struct connman_device *device;
	const char *str;

	_DBG_DEVICE("node %s type %d", node, type);

	device = g_try_new0(struct connman_device, 1);
	if (device == NULL) {
		connman_error("%s: cannot allocate device", __func__);
		return NULL;
	}
	device->networks = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free, unregister_network);
	if (device->networks == NULL) {
		connman_error("%s: cannot allocate hash table", __func__);
		g_free(device);
		return NULL;
	}

	_DBG_DEVICE("device %p", device);

	__connman_element_initialize(&device->element);

	device->element.name = g_strdup(node);
	device->element.type = CONNMAN_ELEMENT_TYPE_DEVICE;

	device->element.device = device;
	device->element.destruct = device_destruct;

	str = type2string(type);
	if (str != NULL)
		connman_element_set_string(&device->element,
					CONNMAN_PROPERTY_ID_TYPE, str);

	device->type      = type;
	device->name      = g_strdup(type2description(device->type));
	device->mode      = CONNMAN_DEVICE_MODE_UNKNOWN;
	device->powered_persistent = TRUE;
	device->phyindex  = -1;

	/* setup RTNL device rename support */
	RTNL_INIT(&device->rtnl,
	    device->name,
	    CONNMAN_RTNL_PRIORITY_DEFAULT,
	    CONNMAN_RTNL_DEVICE_ANY,	/* NB: filled in later */
	    device);
	device->rtnl.newlink = device_newlink;

	switch (type) {
	case CONNMAN_DEVICE_TYPE_WIFI:
		device->scan_method = NULL;
		device->scan_interval = SCAN_INTERVAL_UNSET;
		device->scan_short = SCAN_INTERVAL_UNSET;
		device->signal_threshold = SIGNAL_THRESHOLD_UNSET;
		break;
	default:
		device->scan_interval = 0;
		break;
	}
	return device;
}

/**
 * connman_device_ref:
 * @device: device structure
 *
 * Increase reference counter of device
 */
struct connman_device *connman_device_ref(struct connman_device *device)
{
	connman_element_ref(&device->element);
	return device;
}

/**
 * connman_device_unref:
 * @device: device structure
 *
 * Decrease reference counter of device
 */
void connman_device_unref(struct connman_device *device)
{
	connman_element_unref(&device->element);
}

const char *__connman_device_get_type(struct connman_device *device)
{
	return type2string(device->type);
}

/**
 * connman_device_get_type:
 * @device: device structure
 *
 * Get type of device
 */
enum connman_device_type connman_device_get_type(struct connman_device *device)
{
	return device->type;
}

/**
 * connman_device_get_name:
 * @device: device structure
 *
 * Get unique name of device
 */
const char *connman_device_get_name(struct connman_device *device)
{
	return device->element.name;
}

/**
 * connman_device_get_path:
 * @device: device structure
 *
 * Get path name of device
 */
const char *connman_device_get_path(struct connman_device *device)
{
	return device->element.path;
}

static void device_newlink(void *user_data, int index, unsigned short type,
    const char *ifname, unsigned flags, int change)
{
	struct connman_device *device = user_data;

	if (g_strcmp0(device->interface, ifname) != 0) {
		_DBG_DEVICE("device %p ifname %s -> %s", device,
		    device->interface, ifname);
		connman_device_set_interface(device, ifname, device->control);
	}
}

/**
 * connman_device_set_index:
 * @device: device structure
 * @index: index number
 *
 * Set index number of device
 */
void connman_device_set_index(struct connman_device *device, int index)
{
	if (index == device->element.index)
		return;
	/*
	 * Setup/teardown the RTNL newlink monitor callback
	 * used to track device renaming.
	 */
	if (device->element.index == -1) {
		device->rtnl.index = index;
		connman_rtnl_register(&device->rtnl);
	}
	device->element.index = index;
	if (index == -1) {
		connman_rtnl_unregister(&device->rtnl);
		device->rtnl.index = CONNMAN_RTNL_DEVICE_ANY;
	}
}

/**
 * connman_device_get_index:
 * @device: device structure
 *
 * Get index number of device
 */
int connman_device_get_index(struct connman_device *device)
{
	return device->element.index;
}

int __connman_device_get_phyindex(struct connman_device *device)
{
	return device->phyindex;
}

void __connman_device_set_phyindex(struct connman_device *device,
							int phyindex)
{
	device->phyindex = phyindex;
}

/**
 * connman_device_get_interface:
 * @device: device structure
 *
 * Get interface name for device
 */
const char *connman_device_get_interface(struct connman_device *device)
{
	return device->interface;
}

/**
 * connman_device_set_interface:
 * @device: device structure
 * @interface: interface name
 * @control: control interface
 *
 * Set interface name of device
 */
void connman_device_set_interface(struct connman_device *device,
    const char *interface, const char *control)
{
	g_free(device->element.devname);
	device->element.devname = g_strdup(interface);

	g_free(device->interface);
	device->interface = g_strdup(interface);

	g_free(device->control);
	device->control = g_strdup(control);

	if (device->name == NULL) {
		const char *str = type2description(device->type);
		if (str != NULL && device->interface != NULL)
			device->name = g_strdup_printf("%s (%s)", str,
							device->interface);
	}
}

const char *connman_device_get_control(struct connman_device *device)
{
	return device->control;
}

/**
 * connman_device_set_ident:
 * @device: device structure
 * @ident: unique identifier
 *
 * Set unique identifier of device
 */
void connman_device_set_ident(struct connman_device *device, const char *ident)
{
	g_free(device->ident);
	device->ident = g_strdup(ident);
}

const char *__connman_device_get_ident(struct connman_device *device)
{
	return device->ident;
}

/**
 * connman_device_set_mode:
 * @device: device structure
 * @mode: network mode
 *
 * Change network mode of device
 */
void connman_device_set_mode(struct connman_device *device,
    enum connman_device_mode mode)
{
	device->mode = mode;
}

/**
 * connman_device_get_mode:
 * @device: device structure
 *
 * Get network mode of device
 */
enum connman_device_mode connman_device_get_mode(struct connman_device *device)
{
	return device->mode;
}

/**
 * connman_device_get_selected_network:
 * @device: device structure
 *
 * Get the network ID of the cellular network, if any, on which
 * the user requested manual registration.
 */
const char *connman_device_get_selected_network(struct connman_device *device)
{
	return device->cellular.selected_network;
}

/**
 * connman_device_clear_selected_network:
 * @device: device structure
 *
 * Forget any previously selected cellular network ID, thus causing
 * the next network reigstration attempt to be automatic (i.e., on
 * the home network).
 */
void connman_device_clear_selected_network(struct connman_device *device)
{
	g_free(device->cellular.selected_network);
	device->cellular.selected_network = NULL;
	__connman_profile_save_device(device);
}

/**
 * connman_device_set_powered:
 * @device: device structure
 * @powered: powered state
 *
 * Change power state of device
 */
int connman_device_set_powered(struct connman_device *device,
    connman_bool_t powered)
{
	enum connman_service_type type;

	_DBG_DEVICE("driver %p powered %d", device, powered);

	if (device->timeout > 0) {
		g_source_remove(device->timeout);
		device->timeout = 0;
	}

	if (device->pending != NULL) {
		g_dbus_send_reply(connection, device->pending,
							DBUS_TYPE_INVALID);
		dbus_message_unref(device->pending);
		device->pending = NULL;
	}

	bgscan_disable(device);

	if (device->powered == powered)
		return -EALREADY;

	device->powered = powered;
	device->powered_pending = powered;

	type = __connman_device_get_service_type(device);

	if (device->powered == TRUE)
		__connman_notifier_enable(type);
	else
		__connman_notifier_disable(type);

	if (device->registered == FALSE)
		return 0;

	powered_changed(device);

	if (device->powered == TRUE &&
	    device->type != CONNMAN_DEVICE_TYPE_CELLULAR) {
		connman_device_scan(device);
		bgscan_enable(device);
	}

	return 0;
}

void connman_device_set_powered_failed(struct connman_device *device,
				       enum connman_element_error error)
{
	_DBG_DEVICE("device %s error %d", device->ident, error);

	if (device->timeout > 0) {
		g_source_remove(device->timeout);
		device->timeout = 0;
	}

	if (device->pending != NULL) {
		const char *errname = error_to_dbus_error_name(error);
		const char *msg = device->powered_pending ?
				"Enable Failure" : "Disable Failure";
		DBusMessage *reply;
		reply = g_dbus_create_error(device->pending, errname, msg);
		g_dbus_send_message(connection, reply);
		dbus_message_unref(device->pending);
		device->pending = NULL;
	}
	device->powered_pending = device->powered;
}

int __connman_device_set_blocked(struct connman_device *device,
    connman_bool_t blocked)
{
	connman_bool_t powered;

	_DBG_DEVICE("device %p blocked %d", device, blocked);

	device->blocked = blocked;

	if (device->offlinemode == TRUE)
		return 0;

	if (blocked == FALSE)
		powered = device->powered_persistent;
	else
		powered = FALSE;

	return set_powered(device, powered);
}

/**
 * connman_device_set_carrier:
 * @device: device structure
 * @carrier: carrier state
 *
 * Change carrier state of device (only for device without scanning)
 */
int connman_device_set_carrier(struct connman_device *device,
    connman_bool_t carrier)
{
	_DBG_DEVICE("device %p carrier %d", device, carrier);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		return -EINVAL;
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		break;
	}

	if (device->carrier == carrier)
		return -EALREADY;

	device->carrier = carrier;

	return set_carrier(device, device->carrier);
}

/**
 * connman_device_bgscan_get_short:
 * @device: device structure
 *
 * Return interval (in seconds) for urgent bgscan's done when we believe
 * we want to roam (e.g. the signal level drops below the threshold).
 */
int connman_device_bgscan_get_short(const struct connman_device *device)
{
	if (device->scan_short == SCAN_INTERVAL_UNSET)
		return DEFAULT_SCAN_SHORT;
	return device->scan_short;
}

/**
 * connman_device_bgscan_get_long:
 * @device: device structure
 *
 * Return interval (in seconds) for slow bgscan's done when we our
 * connection is satisfactory (e.g. the signal level is above the threshold).
 */
int connman_device_bgscan_get_long(const struct connman_device *device)
{
	if (device->scan_interval == SCAN_INTERVAL_UNSET)
		return DEFAULT_SCAN_INTERVAL;
	return device->scan_interval;
}

/**
 * connman_device_bgscan_get_method:
 * @device: device structure
 *
 * Return background scan method (string) to use.  This function can return
 * NULL which means "use default".  It is up to the plugin to interpret this
 * string and determine what to do if it does not recognize the string.
 */
const char *connman_device_bgscan_get_method(const struct
                                             connman_device *device)
{
	if (device->scan_method == NULL)
		return DEFAULT_SCAN_METHOD;
	return device->scan_method;
}

/**
 * connman_device_bgscan_get_signal_threshold:
 * @device: device structure
 *
 * Return signal level threshold (in dBm) at which we proactively scan
 * for a new network.
 */
int connman_device_bgscan_get_signal_threshold(const struct connman_device *device)
{
	if (device->signal_threshold == SIGNAL_THRESHOLD_UNSET)
		return DEFAULT_SIGNAL_THRESHOLD;
	return device->signal_threshold;
}

/**
 * connman_device_scan:
 * @device: device structure
 *
 * Kick off a scan for the specified device.  The device must have
 * a driver that supports scan requests and must be pwoered up.
 */
int connman_device_scan(struct connman_device *device)
{
	if (device->driver == NULL || device->driver->scan == NULL)
		return -EOPNOTSUPP;
	if (device->powered == FALSE)
		return -ENOLINK;

	/*
	 * If bgscan is active push the next scan forward so
	 * we don't fire multiple scans too quickly.
	 */
	if (device->scan_timeout != 0)
		bgscan_enable(device);

	return device->driver->scan(device);
}

void connman_device_auto_connect(struct connman_device *device)
{
	__connman_service_device_auto_connect(device);
}

int __connman_device_enable(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	if (device->driver == NULL || device->driver->enable == NULL)
		return -EOPNOTSUPP;

	return set_powered(device, TRUE);
}

int __connman_device_enable_persistent(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	device->powered_persistent = TRUE;

	__connman_profile_save_device(device);

	return __connman_device_enable(device);
}

int __connman_device_disable(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	if (device->driver == NULL || device->driver->disable == NULL)
		return -EOPNOTSUPP;
	if (device->powered == FALSE)
		return -ENOLINK;

	return set_powered(device, FALSE);
}

int __connman_device_disable_persistent(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	device->powered_persistent = FALSE;

	__connman_profile_save_device(device);

	return __connman_device_disable(device);
}

int __connman_device_connect(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	if (device->connected == TRUE)
		return -EALREADY;

	if (device->driver != NULL && device->driver->connect != NULL)
		device->driver->connect(device);
	/* TODO(sleffler) else connman_device_set_connected(device, TRUE)? */

	return 0;
}

int __connman_device_disconnect(struct connman_device *device)
{
	GHashTableIter iter;
	gpointer key, value;

	_DBG_DEVICE("device %p connected %d", device, device->connected);

	if (device->connected == FALSE)
		return -EALREADY;

	g_hash_table_iter_init(&iter, device->networks);
	while (g_hash_table_iter_next(&iter, &key, &value) == TRUE) {
		struct connman_network *network = value;

		__connman_network_disconnect(network);
	}
	if (device->driver != NULL && device->driver->disconnect != NULL)
		device->driver->disconnect(device);
	else
		connman_device_set_connected(device, FALSE);
	return 0;
}

/**
 * connman_device_set_scanning_state:
 * @device: device structure
 * @scanning: scanning state
 *
 * Change scanning state of device
 */
int connman_device_set_scanning_state(struct connman_device *device,
    connman_bool_t scanning)
{
	_DBG_DEVICE("device %p scanning %d", device, scanning);

	if (device->driver == NULL || device->driver->scan == NULL)
		return -EINVAL;
	if (device->scanning == scanning)
		return -EALREADY;

	device->scanning = scanning;

	/* NB: connman_dbus_send_property_changed logs msg on failure */
	(void) connman_dbus_send_property_changed_variant(device->element.path,
	    CONNMAN_DEVICE_INTERFACE, "Scanning", DBUS_TYPE_BOOLEAN, &scanning);
	return 0;
}

static void mark_network_unavailable(gpointer key, gpointer value,
    gpointer user_data)
{
	struct connman_network *network = value;

	if (connman_network_get_in_use(network) == FALSE)
		connman_network_set_available(network, FALSE);
}

void __connman_device_mark_networks_unavailable(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	g_hash_table_foreach(device->networks, mark_network_unavailable, NULL);
}

static gboolean remove_unavailable_network(gpointer key, gpointer value,
    gpointer user_data)
{
	struct connman_network *network = value;

	return (connman_network_get_in_use(network) == FALSE &&
	        connman_network_get_available(network) == FALSE);
}

void __connman_device_cleanup_networks(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	g_hash_table_foreach_remove(device->networks,
	    remove_unavailable_network, NULL);
}

/**
 * connman_device_flush_networks:
 * @device: device structure
 *
 * Remove all !connected networks.
 */
void connman_device_flush_networks(struct connman_device *device)
{
	__connman_device_mark_networks_unavailable(device);
	__connman_device_cleanup_networks(device);
}

/**
 * connman_device_set_scanning:
 * @device: device structure
 * @scanning: scanning state (TRUE = started, FALSE = completed)
 *
 * Change scanning state of device and manage associated
 * networks for devices that do not provide incremental
 * state updates.  Specifically, when a scan is started
 * all networks are marked "unavailable" and when the scan
 * completes any unavailable networks are cleared.
 *
 * When a scan completes and the device is not connected
 * the autoconnect process is scheduled.
 */
int connman_device_set_scanning(struct connman_device *device,
    connman_bool_t scanning)
{
	int err;

	_DBG_DEVICE("device %p scanning %d", device, scanning);

	err = connman_device_set_scanning_state(device, scanning);
	if (err != 0)
		return err;

	if (scanning == TRUE) {
		__connman_device_mark_networks_unavailable(device);
	} else {
		__connman_device_cleanup_networks(device);
		if (device->connected == FALSE)
			__connman_service_device_auto_connect(device);
	}
	return 0;
}

/**
 * connman_device_set_connected:
 * @device: device structure
 * @connected: connected state
 *
 * Change connected state of device
 */
int
connman_device_set_connected(struct connman_device *device,
    connman_bool_t connected)
{
	_DBG_DEVICE("device %p carrier %d connected %d -> %d",
	    device, device->carrier, device->connected, connected);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		if (device->connected == connected)
			return -EALREADY;
		return set_connected(device, connected);
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		/* TODO(sleffler) device->connected == connected? */
		if (device->carrier == FALSE)
			return -ENOTCONN;
		return set_connected(device, connected);
	default:
		return -EINVAL;
	}
}

/**
 * connman_device_set_string:
 * @device: device structure
 * @key: unique identifier
 * @value: string value
 *
 * Set string value for specific key
 */
int connman_device_set_string(struct connman_device *device, const char *key,
    const char *value)
{
	_DBG_DEVICE("device %p key %s value %s", device, key,
		    connman_log_get_masked_value(key, value));

	if (g_str_equal(key, "Address") == TRUE) {
		g_free(device->address);
		device->address = g_strdup(value);
	} else if (g_str_equal(key, "Name") == TRUE) {
		g_free(device->name);
		device->name = g_strdup(value);
	} else if (g_str_equal(key, "Node") == TRUE) {
		g_free(device->node);
		device->node = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.Carrier") == TRUE) {
		g_free(device->cellular.carrier);
		device->cellular.carrier = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.MEID") == TRUE) {
		g_free(device->cellular.meid);
		device->cellular.meid = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.IMEI") == TRUE) {
		g_free(device->cellular.imei);
		device->cellular.imei = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.IMSI") == TRUE) {
		g_free(device->cellular.imsi);
		device->cellular.imsi = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.ESN") == TRUE) {
		g_free(device->cellular.esn);
		device->cellular.esn = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.MDN") == TRUE) {
		g_free(device->cellular.mdn);
		device->cellular.mdn = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.MIN") == TRUE) {
		g_free(device->cellular.min);
		device->cellular.min = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.ModelID") == TRUE) {
		g_free(device->cellular.model_id);
		device->cellular.model_id = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.Manufacturer") == TRUE) {
		g_free(device->cellular.manufacturer);
		device->cellular.manufacturer = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.FirmwareRevision") == TRUE) {
		g_free(device->cellular.firmware_revision);
		device->cellular.firmware_revision = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.FirmwareImageName") == TRUE) {
		g_free(device->cellular.firmware_image);
		device->cellular.firmware_image = g_strdup(value);
	} else if (g_str_equal(key, "Cellular.HardwareRevision") == TRUE) {
		g_free(device->cellular.hardware_revision);
		device->cellular.hardware_revision = g_strdup(value);
	} else if (g_str_equal(key, "DBus.Connection") == TRUE) {
		g_free(device->external_dbus_connection);
		device->external_dbus_connection = g_strdup(value);
	} else if (g_str_equal(key, "DBus.Service") == TRUE) {
		g_free(device->external_dbus_service);
		device->external_dbus_service = g_strdup(value);
	} else if (g_str_equal(key, "DBus.Object") == TRUE) {
		g_free(device->external_dbus_object);
		device->external_dbus_object = g_strdup(value);
	}
	return connman_element_set_string(&device->element, key, value);
}

/**
 * connman_device_get_string:
 * @device: device structure
 * @key: unique identifier
 *
 * Get string value for specific key
 */
const char *connman_device_get_string(struct connman_device *device,
    const char *key)
{
	_DBG_DEVICE("device %p key %s", device, key);

	if (g_str_equal(key, "Address") == TRUE)
		return device->address;
	else if (g_str_equal(key, "Name") == TRUE)
		return device->name;
	else if (g_str_equal(key, "Node") == TRUE)
		return device->node;
	else if (g_str_equal(key, "Cellular.Carrier") == TRUE)
		return device->cellular.carrier;
	else if (g_str_equal(key, "Cellular.MEID") == TRUE)
		return device->cellular.meid;
	else if (g_str_equal(key, "Cellular.IMEI") == TRUE)
		return device->cellular.imei;
	else if (g_str_equal(key, "Cellular.IMSI") == TRUE)
		return device->cellular.imsi;
	else if (g_str_equal(key, "Cellular.ESN") == TRUE)
		return device->cellular.esn;
	else if (g_str_equal(key, "Cellular.MDN") == TRUE)
		return device->cellular.mdn;
	else if (g_str_equal(key, "Cellular.MIN") == TRUE)
		return device->cellular.min;
	else if (g_str_equal(key, "Cellular.ModelID") == TRUE)
		return device->cellular.model_id;
	else if (g_str_equal(key, "Cellular.Manufacturer") == TRUE)
		return device->cellular.manufacturer;
	else if (g_str_equal(key, "Cellular.FirmwareRevision") == TRUE)
		return device->cellular.firmware_revision;
	else if (g_str_equal(key, "Cellular.FirmwareImageName") == TRUE)
		return device->cellular.firmware_image;
	else if (g_str_equal(key, "Cellular.HardwareRevision") == TRUE)
		return device->cellular.hardware_revision;
	else if (g_str_equal(key, "DBus.Connection") == TRUE)
		return device->external_dbus_connection;
	else if (g_str_equal(key, "DBus.Service") == TRUE)
		return device->external_dbus_service;
	else if (g_str_equal(key, "DBus.Object") == TRUE)
		return device->external_dbus_object;
	return connman_element_get_string(&device->element, key);
}

void connman_device_set_cellular_family(struct connman_device *device,
					enum connman_device_cellular_family family)
{
	device->cellular.family = family;
}

void connman_device_set_prl_version(struct connman_device *device,
				    guint prl_version)
{
	device->cellular.prl_version = prl_version;
}

void connman_device_set_unlock_properties(struct connman_device *device,
					  const char *unlock_required,
					  guint unlock_retries,
					  gboolean lock_enabled)
{
	g_free(device->cellular.unlock_required);
	device->cellular.unlock_required = g_strdup(unlock_required);
	device->cellular.unlock_retries = unlock_retries;
	device->cellular.lock_enabled = lock_enabled;
	connman_dbus_send_property_changed_dict(device->element.path,
				  CONNMAN_DEVICE_INTERFACE,
				  "Cellular.SIMLockStatus",
				  append_unlock_status,
				  device);
}

void connman_device_set_found_networks(struct connman_device *device,
				       GPtrArray *networks)
{
	if (device->cellular.found_networks != NULL)
		g_ptr_array_free(device->cellular.found_networks, TRUE);
	device->cellular.found_networks = networks;

	connman_dbus_send_property_changed_container(device->element.path,
				    CONNMAN_DEVICE_INTERFACE,
				    "Cellular.FoundNetworks",
				    kStringDictArraySig,
				    append_string_dict_array,
				    device->cellular.found_networks);
}

void connman_device_set_apn_list(struct connman_device *device,
				 GPtrArray *apn_list)
{
	if (device->cellular.apn_list != NULL)
		g_ptr_array_free(device->cellular.apn_list, TRUE);
	device->cellular.apn_list = apn_list;

	connman_dbus_send_property_changed_container(device->element.path,
				    CONNMAN_DEVICE_INTERFACE,
				    "Cellular.APNList",
				    kStringDictArraySig,
				    append_string_dict_array,
				    device->cellular.apn_list);
}

void connman_device_set_provider_requires_roaming(struct connman_device *device,
						  connman_bool_t requires_roaming)
{
	device->cellular.provider_requires_roaming = requires_roaming;
}

static void set_offlinemode(struct connman_element *element, gpointer user_data)
{
	struct connman_device *device = element->device;
	connman_bool_t offlinemode = GPOINTER_TO_UINT(user_data);
	connman_bool_t powered;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (device == NULL)
		return;

	device->offlinemode = offlinemode;

	powered = (offlinemode == TRUE) ? FALSE : TRUE;

	if (device->powered == powered)
		return;

	if (device->powered_persistent == FALSE)
		powered = FALSE;

	set_powered(device, powered);
}

int __connman_device_set_offlinemode(connman_bool_t offlinemode)
{
	_DBG_DEVICE("offlinmode %d", offlinemode);

	__connman_element_foreach(NULL, CONNMAN_ELEMENT_TYPE_DEVICE,
			set_offlinemode, GUINT_TO_POINTER(offlinemode));

	__connman_notifier_offlinemode(offlinemode);

	return 0;
}


/**
 * __connman_device_set_operator:
 * @device: device structure
 * @info: operator structure
 *
 * Set operator information structure for the service
 */
void connman_device_set_home_provider(struct connman_device *device,
                                      struct connman_network_operator *op)
{
        g_free(device->cellular.home_provider.name);
        device->cellular.home_provider.name = g_strdup(op->name);
        g_free(device->cellular.home_provider.code);
        device->cellular.home_provider.code = g_strdup(op->code);
        g_free(device->cellular.home_provider.country);
        device->cellular.home_provider.country = g_strdup(op->country);
}

struct connman_network_operator *connman_device_get_home_provider(
        struct connman_device *device)
{
        if (device->cellular.home_provider.name == NULL)
                return NULL;
        return &device->cellular.home_provider;
}

connman_bool_t connman_device_roaming_allowed(struct connman_device *device)
{
	return device->cellular.roaming_allowed ||
		device->cellular.provider_requires_roaming;
}

void connman_device_set_scanning_supported(struct connman_device *device,
					   connman_bool_t supported)
{
	device->cellular.scanning_supported = supported;
}


/**
 * connman_device_add_network:
 * @device: device structure
 * @network: network structure
 *
 * Add new network to the device
 */
int connman_device_add_network(struct connman_device *device,
    struct connman_network *network)
{
	const char *identifier = connman_network_get_identifier(network);
	int err;

	_DBG_DEVICE("device %p network %p", device, network);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		return -EINVAL;
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		break;
	}

	__connman_network_set_device(network, device);

	err = connman_element_register((struct connman_element *) network,
							&device->element);
	if (err < 0) {
		__connman_network_set_device(network, NULL);
		return err;
	}

	g_hash_table_insert(device->networks, g_strdup(identifier), network);

	return 0;
}

/**
 * connman_device_get_network:
 * @device: device structure
 * @identifier: network identifier
 *
 * Get network for given identifier
 */
struct connman_network *connman_device_get_network(
    struct connman_device *device, const char *identifier)
{
	_DBG_DEVICE("device %p identifier %s", device, identifier);

	return g_hash_table_lookup(device->networks, identifier);
}

/**
 * connman_device_remove_network:
 * @device: device structure
 * @identifier: network identifier
 *
 * Remove network for given identifier
 */
int connman_device_remove_network(struct connman_device *device,
    const char *identifier)
{
	_DBG_DEVICE("device %p identifier %s", device, identifier);

	/* NB: this implicitly calls unregister_network */
	g_hash_table_remove(device->networks, identifier);

	return 0;
}

struct connman_network *__connman_device_get_current_network(
    struct connman_device *device)
{
	return device->network;
}

void __connman_device_set_current_network(struct connman_device *device,
    struct connman_network *network)
{
	const char *bssid = NULL;

	_DBG_DEVICE("device %p network %p existing %p", device,
	    network, device->network);

	if (network != NULL)
		bssid = connman_network_get_string(network, "Address");
	if (device->network == network) {
		if (network == NULL)
			return;
		/*
		 * NB: roaming for old supplicant clobbers the bssid of the
		 * current network so we must compare addresses.
		 */
		if (g_strcmp0(device->bssid, bssid) != 0) {
			connman_info("%s: ROAMING %s -> %s", device->interface,
			    device->bssid, bssid);

			g_free(device->bssid);
			device->bssid = g_strdup(bssid);
			/* NB: this will kick off L3 re-configuration */
			set_connected(device, TRUE);
		} else {
			/*
			 * Same BSSID; this is what happens when we
			 * re-associate (e.g. due to idle timeout).
			 */
			connman_ipconfig_renew(device);
		}
		return;
	}
	if (network != NULL && device->network != NULL) {
		/*
		 * Roaming with new supplicant plugin; we get a proper
		 * network handle instead of clobbering the bssid of the
		 * same network object.  Same tasks as above, just more
		 * straightforward
		 */
		connman_info("%s: ROAMING %s -> %s", device->interface,
		    device->bssid, bssid);

		connman_network_unref(device->network);
		device->network = connman_network_ref(network);
		g_free(device->bssid);
		device->bssid = g_strdup(bssid);
		set_connected(device, TRUE);
	} else {
		/*
		 * Not roaming; either clear state or setup fresh.
		 */
		if (device->network != NULL)
			connman_network_unref(device->network);

		if (network != NULL) {
			device->network = connman_network_ref(network);
			g_free(device->bssid);
			device->bssid = g_strdup(bssid);
			connman_device_set_connected(device, TRUE);
		} else {
			g_free(device->bssid);
			device->bssid = NULL;
			connman_device_set_connected(device, FALSE);
			/*
			 * Don't null out the network until after the
			 * preceding call to connman_device_set_connected.
			 * The latter results in the ipconfig being released,
			 * which may remove the default gateway. When that
			 * happens, we want to notify the service associated
			 * with the device, but we will be unable to find it
			 * if the link between device and network has been
			 * broken.
			 */
			device->network = NULL;
		}
	}
}

void __connman_device_set_reconnect(struct connman_device *device,
	connman_bool_t onoff)
{
	device->reconnect = onoff;
}

connman_bool_t __connman_device_get_reconnect(struct connman_device *device)
{
	return device->reconnect;
}

/**
 * connman_device_register:
 * @device: device structure
 *
 * Register device with the system
 */
int connman_device_register(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	__connman_profile_load_device(device);

	device->offlinemode = __connman_profile_get_offlinemode();

	return connman_element_register(&device->element, NULL);
}

/**
 * connman_device_unregister:
 * @device: device structure
 *
 * Unregister device with the system
 */
void connman_device_unregister(struct connman_device *device)
{
	_DBG_DEVICE("device %p", device);

	__connman_profile_save_device(device);

	connman_element_unregister(&device->element);
}

/**
 * connman_device_get_data:
 * @device: device structure
 *
 * Get private device data pointer
 */
void *connman_device_get_data(struct connman_device *device)
{
	return device->driver_data;
}

/**
 * connman_device_set_data:
 * @device: device structure
 * @data: data pointer
 *
 * Set private device data pointer
 */
void connman_device_set_data(struct connman_device *device, void *data)
{
	device->driver_data = data;
}

static gboolean match_driver(const struct connman_device *device,
    const struct connman_device_driver *driver)
{
	return (device->type == driver->type ||
	    driver->type == CONNMAN_DEVICE_TYPE_UNKNOWN);
}

static int device_probe(struct connman_element *element)
{
	struct connman_device *device = element->device;
	GSList *list;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (device == NULL)
		return -ENODEV;

	if (device->driver != NULL)
		return -EALREADY;

	for (list = driver_list; list; list = list->next) {
		struct connman_device_driver *driver = list->data;

		if (match_driver(device, driver) == FALSE)
			continue;

		_DBG_DEVICE("driver %p name %s", driver, driver->name);

		if (driver->probe(device) == 0)
			return setup_device(device, driver);
	}
	return -ENODEV;
}

static void device_remove(struct connman_element *element)
{
	struct connman_device *device = element->device;

	_DBG_DEVICE("element %p name %s", element, element->name);

	if (device != NULL && device->driver != NULL)
		remove_device(device);
}

static struct connman_driver device_driver = {
	.name		= "device",
	.type		= CONNMAN_ELEMENT_TYPE_DEVICE,
	.priority	= CONNMAN_DRIVER_PRIORITY_LOW,
	.probe		= device_probe,
	.remove		= device_remove,
};

/*
 * Create any ipconfig records for a device.
 */
static connman_bool_t device_load_ipconfigs(struct connman_device *device,
    gsize nconfigs, gchar **configs)
{
	int i;

	for (i = 0; i < nconfigs; i++) {
		enum connman_ipconfig_type type;
		struct connman_ipconfig *ipconfig;
		char *str;
		int ix;

		/* Syntax: <unique-tag>:<method>, e.g. "ipconfig_xxx_1:dhcp" */
		str = strchr(configs[i], ':');
		if (str == NULL) {
			connman_error("%s: no Method for %s", __func__,
			    configs[i]);
			return FALSE;
		}
		*str++ = '\0';
		if (!__connman_ipconfig_parse_type(str, &type)) {
			connman_error("%s: unknown Method %s", __func__, str);
			return FALSE;
		}
		ix = parse_ipconfig_index(configs[i]);
		if (!(0 <= ix && ix < 32)) {
			connman_error("%s: cannot parse index from ipconfig "
			    "name %s", __func__, configs[i]);
			return FALSE;
		}
		if ((device->configmask & (1<<ix)) != 0) {
			/* NB: just ignore for backwards compatibility */
			connman_warn("%s: ignore duplicate ipconfig record "
			    "with name %s", __func__, configs[i]);
			continue;
		}

		ipconfig = connman_ipconfig_create(type, configs[i], device,
		    device->element.index, FALSE);
		if (ipconfig == NULL) {
			connman_error("%s: invalid ipconfig type %s (%d)",
			    __func__, str, type);
			return FALSE;
		}
		device->configmask |= 1<<ix;
	}
	return TRUE;
}

static int device_load(struct connman_device *device, GKeyFile *keyfile)
{
	GError *error = NULL;
	gchar *identifier;
	gchar *method;
	gchar *selected_network;
	connman_bool_t powered;
	connman_bool_t roaming_allowed;
	int val;
	gchar **configs;
	gsize nconfigs;
	int err = 0;

	_DBG_DEVICE("device %p", device);

	identifier = g_strdup_printf("device_%s", device->element.name);
	if (identifier == NULL) {
		err = -ENOMEM;
		goto done;
	}

	if (g_key_file_has_group(keyfile, identifier) == FALSE) {
		err = -ESRCH;
		goto done;
	}

	powered = g_key_file_get_boolean(keyfile, identifier,
						"Powered", &error);
	if (error == NULL)
		device->powered_persistent = powered;
	g_clear_error(&error);

	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		break;
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		val = g_key_file_get_integer(keyfile, identifier,
						"ScanInterval", &error);
		if (error == NULL && val > 0)
			device->scan_interval = val;
		g_clear_error(&error);
		method = g_key_file_get_string(keyfile, identifier,
						"BgscanMethod", &error);
		if (error == NULL && method != NULL) {
			g_free(device->scan_method);
			device->scan_method = method;
		}
		g_clear_error(&error);
		val = g_key_file_get_integer(keyfile, identifier,
						"BgscanShortInterval", &error);
		if (error == NULL && val > 0)
			device->scan_short = val;
		g_clear_error(&error);
		val = g_key_file_get_integer(keyfile, identifier,
						"BgscanSignalThreshold", &error);
		if (error == NULL)
			device->signal_threshold = val;
		g_clear_error(&error);
		roaming_allowed = g_key_file_get_boolean(keyfile,
				identifier, "Cellular.AllowRoaming", &error);
		if (error == NULL)
			device->cellular.roaming_allowed = roaming_allowed;
		else
			device->cellular.roaming_allowed = FALSE;
		g_clear_error(&error);
		selected_network = g_key_file_get_string(keyfile, identifier,
					 "Cellular.SelectedNetwork", &error);
		if (error == NULL) {
			g_free(device->cellular.selected_network);
			device->cellular.selected_network = selected_network;
		} else {
			g_free(selected_network);
		}
		g_clear_error(&error);
		break;
	}

	configs = g_key_file_get_string_list(keyfile, identifier, "IPConfigs",
	    &nconfigs, &error);
	if (error == NULL && configs != NULL)
		device_load_ipconfigs(device, nconfigs, configs);
	g_strfreev(configs);
	g_clear_error(&error);
done:
	g_free(identifier);

	return err;
}

static int device_save(struct connman_device *device, GKeyFile *keyfile)
{
	gchar *identifier;
	const char const *paths[32];
	int npaths;
	int i;

	_DBG_DEVICE("device %p", device);

	identifier = g_strdup_printf("device_%s", device->element.name);
	if (identifier == NULL)
		goto done;

	g_key_file_set_boolean(keyfile, identifier,
					"Powered", device->powered_persistent);
	switch (device->mode) {
	case CONNMAN_DEVICE_MODE_UNKNOWN:
	case CONNMAN_DEVICE_MODE_TRANSPORT_IP:
		break;
	case CONNMAN_DEVICE_MODE_NETWORK_SINGLE:
	case CONNMAN_DEVICE_MODE_NETWORK_MULTIPLE:
		if (device->scan_interval != SCAN_INTERVAL_UNSET)
			g_key_file_set_integer(keyfile, identifier,
			    "ScanInterval", device->scan_interval);
		else
			g_key_file_remove_key(keyfile, identifier,
			    "ScanInterval", NULL);
		if (device->scan_method != NULL)
			g_key_file_set_string(keyfile, identifier,
			    "BgscanMethod", device->scan_method);
		else
			g_key_file_remove_key(keyfile, identifier,
			    "BgscanMethod", NULL);
		if (device->scan_short != SCAN_INTERVAL_UNSET)
			g_key_file_set_integer(keyfile, identifier,
			    "BgscanShortInterval", device->scan_short);
		else
			g_key_file_remove_key(keyfile, identifier,
			    "BgscanShortInterval", NULL);
		if (device->signal_threshold != SIGNAL_THRESHOLD_UNSET)
			g_key_file_set_integer(keyfile, identifier,
			    "BgscanSignalThreshold", device->signal_threshold);
		else
			g_key_file_remove_key(keyfile, identifier,
			    "BgscanSignalThreshold", NULL);
		if (device->type == CONNMAN_DEVICE_TYPE_CELLULAR) {
			g_key_file_set_boolean(keyfile, identifier,
					       "Cellular.AllowRoaming",
					       device->cellular.roaming_allowed);
			if (device->cellular.selected_network != NULL)
				g_key_file_set_string(keyfile, identifier,
					       "Cellular.SelectedNetwork",
					       device->cellular.selected_network);
			else
				g_key_file_remove_key(keyfile, identifier,
					      "Cellular.SelectedNetwork", NULL);
		}
		break;
	}

	__connman_ipconfig_get_keys(device, 32, paths, &npaths);
	g_key_file_set_string_list(keyfile, identifier, "IPConfigs",
	    paths, npaths);
	for (i = 0; i < npaths; i++)
		g_free((gpointer)paths[i]);
done:
	g_free(identifier);

	return 0;
}

static struct connman_storage device_storage = {
	.name		= "device",
	.priority	= CONNMAN_STORAGE_PRIORITY_LOW,
	.device_load	= device_load,
	.device_save	= device_save,
};

int __connman_device_init(void)
{
	/* TODO(sleffler) check return values */
	connection = connman_dbus_get_connection();
	if (connman_storage_register(&device_storage) < 0)
		connman_error("%s: failed to register storage", __func__);
	return connman_driver_register(&device_driver);
}

void __connman_device_cleanup(void)
{
	connman_driver_unregister(&device_driver);
	connman_storage_unregister(&device_storage);
	dbus_connection_unref(connection);
}
